{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# Components with hierarchy\n",
    "\n",
    "![](https://i.imgur.com/3pczkyM.png)\n",
    "\n",
    "You can define component parametric cells (waveguides, bends, couplers) as functions with basic input parameters (width, length, radius ...) and use them as arguments for composing more complex functions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "from functools import partial\n",
    "\n",
    "import toolz\n",
    "\n",
    "import gdsfactory as gf\n",
    "from gdsfactory.typings import ComponentSpec\n",
    "from gdsfactory.cross_section import CrossSectionSpec"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "**Problem**\n",
    "\n",
    "When using hierarchical cells where you pass `N` subcells with `M` parameters you can end up with `N*M` parameters. This will make it hard to read the code.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "@gf.cell\n",
    "def bend_with_straight_with_too_many_input_parameters(\n",
    "    bend=gf.components.bend_euler,\n",
    "    straight=gf.components.straight,\n",
    "    length: float = 3,\n",
    "    angle: float = 90.0,\n",
    "    p: float = 0.5,\n",
    "    with_arc_floorplan: bool = True,\n",
    "    npoints: int | None = None,\n",
    "    cross_section: CrossSectionSpec = \"strip\",\n",
    ") -> gf.Component:\n",
    "    \"\"\"As hierarchical cells become more complex, the number of input parameters can increase significantly.\"\"\"\n",
    "    c = gf.Component()\n",
    "    b = bend(\n",
    "        angle=angle,\n",
    "        p=p,\n",
    "        with_arc_floorplan=with_arc_floorplan,\n",
    "        npoints=npoints,\n",
    "        cross_section=cross_section,\n",
    "    )\n",
    "    s = straight(length=length, cross_section=cross_section)\n",
    "\n",
    "    bref = c << b\n",
    "    sref = c << s\n",
    "\n",
    "    sref.connect(\"o2\", bref.ports[\"o2\"])\n",
    "    c.info[\"length\"] = b.info[\"length\"] + s.info[\"length\"]\n",
    "    return c\n",
    "\n",
    "\n",
    "c = bend_with_straight_with_too_many_input_parameters()\n",
    "c.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "**Solution**\n",
    "\n",
    "You can use a ComponentSpec parameter for every subcell. The ComponentSpec can be a dictionary with arbitrary number of settings, a string, or a function.\n",
    "\n",
    "## ComponentSpec\n",
    "\n",
    "When defining a `Parametric cell` you can use other `ComponentSpec` as an argument. It can be a:\n",
    "\n",
    "1. string: function name of a cell registered on the active PDK. `\"bend_circular\"`.\n",
    "2. dict: `dict(component='bend_circular', settings=dict(radius=20))`.\n",
    "3. function: Using `functools.partial` you can customize the default parameters of a function.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {},
   "outputs": [],
   "source": [
    "@gf.cell\n",
    "def bend_with_straight(\n",
    "    bend: ComponentSpec = gf.components.bend_euler,\n",
    "    straight: ComponentSpec = gf.components.straight,\n",
    ") -> gf.Component:\n",
    "    \"\"\"Much simpler version.\n",
    "\n",
    "    Args:\n",
    "        bend: input bend.\n",
    "        straight: output straight.\n",
    "    \"\"\"\n",
    "    c = gf.Component()\n",
    "    b = gf.get_component(bend)\n",
    "    s = gf.get_component(straight)\n",
    "\n",
    "    bref = c << b\n",
    "    sref = c << s\n",
    "\n",
    "    sref.connect(\"o2\", bref.ports[\"o2\"])\n",
    "    c.info[\"length\"] = b.info[\"length\"] + s.info[\"length\"]\n",
    "    return c\n",
    "\n",
    "\n",
    "c = bend_with_straight()\n",
    "c.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "### 1. String\n",
    "\n",
    "You can use any string registered in the `PDK`. Go to the PDK tutorial to learn how to register cells in a PDK."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7",
   "metadata": {},
   "outputs": [],
   "source": [
    "c = bend_with_straight(bend=\"bend_circular\")\n",
    "c.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "### 2. Dictionary\n",
    "\n",
    "You can pass a `dict` of settings."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9",
   "metadata": {},
   "outputs": [],
   "source": [
    "bend = gf.get_component(\"bend_circular\", radius=20)\n",
    "\n",
    "c = bend_with_straight(bend=bend)\n",
    "c.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10",
   "metadata": {},
   "source": [
    "### 3. Function\n",
    "\n",
    "You can pass a function of a function with customized default input parameters `from functools import partial`.\n",
    "\n",
    "Partial lets you define different default parameters for a function, so you can modify the default settings for each child cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11",
   "metadata": {},
   "outputs": [],
   "source": [
    "c = bend_with_straight(bend=partial(gf.components.bend_circular, radius=30))\n",
    "c.plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "12",
   "metadata": {},
   "outputs": [],
   "source": [
    "bend20 = partial(gf.components.bend_circular, radius=20)\n",
    "b = bend20()\n",
    "b.plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13",
   "metadata": {},
   "outputs": [],
   "source": [
    "type(bend20)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14",
   "metadata": {},
   "outputs": [],
   "source": [
    "bend20.func.__name__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15",
   "metadata": {},
   "outputs": [],
   "source": [
    "bend20.keywords"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16",
   "metadata": {},
   "outputs": [],
   "source": [
    "b = bend_with_straight(bend=bend20)\n",
    "print(b.info.length)\n",
    "b.plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17",
   "metadata": {},
   "outputs": [],
   "source": [
    "# You can still modify the bend to have any bend radius.\n",
    "b3 = bend20(radius=10)\n",
    "b3.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18",
   "metadata": {},
   "source": [
    "## Composing functions\n",
    "\n",
    "You can combine more complex functions out of smaller functions.\n",
    "\n",
    "Let us say that we want to add tapers and grating couplers to a wide waveguide:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19",
   "metadata": {},
   "outputs": [],
   "source": [
    "c1 = gf.components.straight()\n",
    "c1.plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20",
   "metadata": {},
   "outputs": [],
   "source": [
    "straight_wide = partial(gf.components.straight, width=3)\n",
    "c3 = straight_wide()\n",
    "c3.plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21",
   "metadata": {},
   "outputs": [],
   "source": [
    "c1 = gf.components.straight()\n",
    "c1.plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "22",
   "metadata": {},
   "outputs": [],
   "source": [
    "c2 = gf.c.extend_ports(c1, length=5)\n",
    "c2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23",
   "metadata": {},
   "outputs": [],
   "source": [
    "c3 = gf.routing.add_fiber_array(c2, with_loopback=False)\n",
    "c3.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "24",
   "metadata": {},
   "source": [
    "Now we do it with a **single** step thanks to `toolz.pipe`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "25",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "# The partial function is used to create simplified, pre-configured versions of other functions.\n",
    "# add_fiber_array: A new version of the function that adds grating couplers, but with the with_loopback option always set to False.\n",
    "# add_tapers: A new function that extends ports, but is pre-configured to always use the defined taper component as the extension.\n",
    "add_fiber_array = partial(gf.routing.add_fiber_array, with_loopback=False)\n",
    "\n",
    "c1 = gf.c.straight(width=5)\n",
    "taper = gf.components.taper(length=10, width1=5, width2=0.5)\n",
    "add_tapers = partial(gf.c.extend_ports, extension=taper)\n",
    "\n",
    "# Pipe is more readable than the equivalent add_fiber_array(add_tapers(c1)).\n",
    "# The toolz.pipe function takes an initial object (c1) and \"pipes\" it through a series of functions. The output of one function becomes the input for the next.\n",
    "c3 = toolz.pipe(c1, add_tapers, add_fiber_array)\n",
    "c3.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26",
   "metadata": {},
   "source": [
    "we can even combine `add_tapers` and `add_fiber_array` thanks to `toolz.compose` or `toolz.compose`\n",
    "\n",
    "For example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "27",
   "metadata": {},
   "outputs": [],
   "source": [
    "add_tapers_fiber_array = toolz.compose_left(add_tapers, add_fiber_array)\n",
    "c4 = add_tapers_fiber_array(c1)\n",
    "c4.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "28",
   "metadata": {},
   "source": [
    "is equivalent to:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "29",
   "metadata": {},
   "outputs": [],
   "source": [
    "c5 = add_fiber_array(add_tapers(c1))\n",
    "c5.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "30",
   "metadata": {},
   "source": [
    "which is the same as:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "31",
   "metadata": {},
   "outputs": [],
   "source": [
    "add_tapers_fiber_array = toolz.compose(add_fiber_array, add_tapers)\n",
    "c6 = add_tapers_fiber_array(c1)\n",
    "c6.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32",
   "metadata": {},
   "source": [
    "or:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "33",
   "metadata": {},
   "outputs": [],
   "source": [
    "c7 = toolz.pipe(c1, add_tapers, add_fiber_array)\n",
    "c7.plot()"
   ]
  }
 ],
 "metadata": {
  "jupytext": {
   "cell_metadata_filter": "-all",
   "custom_cell_magics": "kql"
  },
  "kernelspec": {
   "display_name": "base",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
